日志数据库存储方案:winston-mongoDB(集中式、自滚动)
为什么选择 MongoDB 作为日志存储
在之前的章节中,我们使用 winston-daily-rotate-file 将日志写入本地文件。这种方式存在几个明显的问题:
- 占用磁盘 IO:高频日志写入会与业务争抢磁盘资源
- 日志分散:多台服务器部署时,日志散落在不同机器上,排查困难
- 检索不便:纯文本日志不支持结构化查询,筛选效率低
将日志写入 MongoDB 可以很好地解决这些问题。winston-mongodb 是 winston 官方推荐的 MongoDB Transport 插件,它将 winston 的日志采集与 MongoDB 的存储解耦,让 winston 专注日志采集,插件负责持久化。
安装依赖
pnpm add winston-mongodb
bash
当前版本为 5.1.1,与 winston 3.x 完全兼容。
环境变量配置
在 .env 文件中添加 MongoDB 日志相关配置项:
# 日志开关
LOG_ON=true
# 日志类型:file | mongo
LOG_TYPE=mongo
# MongoDB 连接地址
LOG_DB=mongodb://root:rootexample@localhost:27017/nest_logs?authSource=admin
# 默认 collection 名称
LOG_COLLECTION=log
# 日志级别:error | warn | info | debug
LOG_LEVEL=error
# 是否记录主机名(便于多实例区分来源)
LOG_STORE_HOST=true
# Capped Collection 配置
LOG_CAPPED=false
LOG_CAPPED_SIZE=10000000
LOG_CAPPED_MAX=
bash
配置要点说明:
LOG_DB:MongoDB 连接字符串,包含数据库名称、用户名和密码。建议为日志创建独立的数据库LOG_COLLECTION:MongoDB 中存储日志的集合名称,默认为logLOG_LEVEL:控制写入 MongoDB 的最低日志级别,生产环境建议设为error或warnLOG_STORE_HOST:设为true后,每条日志会自动附加hostname字段,方便在多实例部署时追溯日志来源LOG_CAPPED:是否启用固定大小集合(Capped Collection),后续章节会详细讨论
创建 MongoDB Transport
在日志模块中创建 createMongoTransport 函数:
import { MongoDB } from 'winston-mongodb'
export function createMongoTransport(options: any) {
return new MongoDB(options)
}
typescript
winston-mongodb 的 MongoDB 类继承自 winston 的 Transport,它接收的 options 参数包括:
| 参数 | 类型 | 说明 |
|---|---|---|
db | string | MongoDB 连接字符串 |
collection | string | 集合名称,默认 log |
level | string | 日志级别 |
capped | boolean | 是否启用固定集合 |
cappedSize | number | 固定集合大小(字节),默认约 10MB |
cappedMax | number | 固定集合最大文档数 |
storeHost | boolean | 是否记录主机名 |
silent | boolean | 是否静默模式 |
在 LogsModule 中集成
修改 LogsModule,根据 LOG_TYPE 配置动态选择 Transport:
import { transports } from 'winston'
import { createMongoTransport } from './create-mongo-transport'
@Injectable()
export class LogsService {
constructor(private configService: ConfigService) {}
createLogger() {
const logOn = this.configService.get<boolean>('LOG_ON')
const logType = this.configService.get<string>('LOG_TYPE')
const transportArr: any[] = [new transports.Console()]
if (logOn) {
if (logType === 'mongo') {
const mongoOptions = {
db: this.configService.get<string>('LOG_DB'),
collection: this.configService.get<string>('LOG_COLLECTION'),
level: this.configService.get<string>('LOG_LEVEL'),
storeHost: this.configService.get<boolean>('LOG_STORE_HOST'),
capped: this.configService.get<boolean>('LOG_CAPPED'),
cappedSize: parseInt(
this.configService.get<string>('LOG_CAPPED_SIZE') || '10000000'
),
options: {
poolSize: 5,
autoReconnect: true,
},
}
transportArr.push(createMongoTransport(mongoOptions))
} else {
// file transport...
}
}
return createLogger({ transports: transportArr })
}
}
typescript
MongoDB 连接选项注意事项
在配置 MongoDB 客户端连接参数时,有几个常见的兼容性问题需要注意:
options: {
poolSize: 5, // 连接池大小
autoReconnect: true, // 自动重连
}
typescript
如果遇到 autoReconnect 与 unifiedTopology 不兼容的警告,可以尝试移除 autoReconnect 参数,新版 MongoDB 驱动已内置重连机制。
创建 MongoDB 数据库和用户
在 Docker 环境中,需要为日志数据库创建专用用户:
# 进入 MongoDB 容器
docker exec -it starter-mongo-1 mongo -u root -p rootexample
# 切换到日志数据库
use nest_logs
# 创建专用用户
db.createUser({
user: 'nest_logs_user',
pwd: 'your_password',
roles: [{ role: 'readWrite', db: 'nest_logs' }]
})
bash
验证日志写入
- 在业务代码中注入 Logger 并输出日志:
import { Logger } from '@nestjs/common'
@Controller('auth')
export class AuthController {
private logger = new Logger()
@Post('register')
async register() {
this.logger.error('注册测试 log')
// ...
}
}
typescript
- 发起请求后,在 MongoDB 中查看
log集合:
{
"_id": "ObjectId(...)",
"timestamp": "2024-01-01T12:00:00.000Z",
"level": "error",
"message": "注册测试 log",
"hostname": "server-01"
}
json
hostname 字段在 storeHost 设为 true 时自动附加,可以精确区分日志来源服务器。
小结
使用 MongoDB + winston-mongodb 的日志存储方案具有以下优势:
- 集中式存储:多台服务器的日志统一写入一个数据库,便于汇总和查询
- 结构化查询:支持按 level、timestamp、hostname 等字段进行条件筛选
- 解耦设计:winston 专注日志采集,插件负责存储,符合单一职责原则
- 配置灵活:通过环境变量控制日志级别、存储类型、主机名记录等
不过这种方案也引入了新的问题:日志数据不断增长可能导致数据库存储空间耗尽。下一节将讨论日志的滚动存储与维护机制。
↑